unit testlogic;

{$mode delphi}

interface

uses
  // RTL, FCL, LCL
  Classes, SysUtils, LCLType, LCLProc,
  // TesteDeVisao
  constants, drawer, config, dbmanager;

type
  TTestPhase = (tpFirstFit, tpSecondFit, tpFinished);

  TTestLogicState = record
    BestFit: Byte;    // Used only in the second fit
    FailedLine: Byte; // Used only in the second fit
    WrongCount: Byte; // Amount of wrong answers in the same line
    CurrentRow: Byte; // Current position
    CurrentCol: Byte; // Current position
    CurrentStep: Byte;// Increased by 1 in every input
    CurrentPhase: TTestPhase;
  end;

  { TTestLogic }

  TTestLogic = class(TObject)
  private
  public
    State: TTestLogicState;
    Drawer: TTestDrawer;
    Instructions: string;
    { In Event handlers }
    OnUpdateUI: TNotifyEvent;
    { Methods }
    constructor Create;
    function  GetRandomDirection(): TDirection;
    procedure NewTest;
    procedure FinishTest;
    function  IsTestRunning: Boolean;
    procedure NextStep(AUserInput: TDirection);
    function GetNextHigherSize(var ASize: Word): Boolean;
    function GetNextLowerSize(var ASize: Word): Boolean;
    { Out Event handlers }
    procedure HandleKeyPress(Sender: TObject; var Key: char);
    procedure HandleKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure HandleKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
  end;

var
  gTestLogic: TTestLogic;

implementation

{ TTestLogic }

constructor TTestLogic.Create;
begin
  inherited Create;

  State.CurrentPhase := tpFinished;
end;

function TTestLogic.GetRandomDirection(): TDirection;
var
  l: Integer;
begin
  l := Trunc(Random() * INT_TDIRECTION_COUNT);
  if l > INT_TDIRECTION_MAX then l := INT_TDIRECTION_MAX;
  Result := TDirection(l);
end;

procedure TTestLogic.NewTest;
var
  i, j: Integer;
begin
  State.BestFit := 0;
  State.FailedLine := 255;
  State.CurrentRow := 0;
  State.CurrentCol := 0;
  State.CurrentPhase := tpFirstFit;
  State.WrongCount := 0;
  State.CurrentStep := 0;
  Instructions := Format(Str_Instruction_Step, [State.CurrentStep]);

  // Builds the table of optotype directions
  for i := 0 to 9 do
    for j := 0 to OptotypeAmounts[i] - 1 do
      Optotypes[i][j] := GetRandomDirection();

  Drawer.UpdateImage;
  OnUpdateUI(Self);
end;

procedure TTestLogic.FinishTest;
var
  lVA: Cardinal;
begin
  State.BestFit := State.CurrentRow;
  State.CurrentPhase := tpFinished;

  lVA := gTestConfig.GetVisualAcuity(State.BestFit);
  gTestLogic.Instructions :=
    Format(Str_Instruction_VA, [lVA]);

  // Add to database
  gDBManager.AddTestResult(lVA);
end;

function TTestLogic.IsTestRunning: Boolean;
begin
  Result := (State.CurrentPhase <> tpFinished);
end;

procedure TTestLogic.NextStep(AUserInput: TDirection);
var
  CurrentDirection: TDirection;
begin
  // Avoid input after test finished
  if State.CurrentPhase = tpFinished then Exit;

  Inc(State.CurrentStep);
  Instructions := Format(Str_Instruction_Step, [State.CurrentStep]);

  CurrentDirection := Optotypes[State.CurrentRow][State.CurrentCol];

  { tpFirstFit phase

    In this fase we do a quick swap in the symbols to have a big picture
    of the user vision. }
  if State.CurrentPhase = tpFirstFit then
  begin
    { The user input was correct }
    if AUserInput = CurrentDirection then
    begin
      { If we are in the last line and the input is correct,
        go to the second element of the line, to avoid asking
        the user for the same optotype twice consecutively }
      if State.CurrentRow = INT_TESTE_MAX_ROW then
      begin
        State.CurrentPhase := tpSecondFit;
        Inc(State.CurrentCol);
      end
      else
        Inc(State.CurrentRow);
    end
    { The user input was incorrect }
    else
    begin
      State.CurrentPhase := tpSecondFit;
      if State.CurrentRow = INT_TESTE_MIN_ROW then
      begin
        State.WrongCount := 1;
        State.CurrentCol := 1;
      end
      else
        Dec(State.CurrentRow);
    end;
  end
  { tpSecondFit fase

    Now we check all symbols of the current row and decide where
    to go next based on the full test of each line.
  }
  else if State.CurrentPhase = tpSecondFit then
  begin
    { The user input was correct }
    if AUserInput = CurrentDirection then
    begin
      if (State.CurrentCol = OptotypeAmounts[State.CurrentRow] - 1) then
      begin
        if State.CurrentRow = INT_TESTE_MAX_ROW then FinishTest()
        else
        begin
          State.BestFit := State.CurrentRow;
          if State.FailedLine = State.CurrentRow + 1 then FinishTest()
          else
          begin
            Inc(State.CurrentRow);
            State.CurrentCol := 0;
            State.WrongCount := 0;
          end;
        end;
      end
      else
        Inc(State.CurrentCol);
    end
    { The user input was incorrect }
    else
    begin
      Inc(State.WrongCount);

      if State.WrongCount > INT_MAX_ERRORS_IN_LINE then
      begin
        if State.CurrentRow = 0 then FinishTest()
        else
        begin
          State.FailedLine := State.CurrentRow;
          if State.BestFit = State.CurrentRow - 1 then FinishTest()
          else
          begin
            Dec(State.CurrentRow);
            State.CurrentCol := 0;
            State.WrongCount := 0;
          end;
        end;
      end
      else
      begin
        if State.CurrentCol = OptotypeAmounts[State.CurrentRow] - 1 then
          FinishTest()
        else
          Inc(State.CurrentCol);
      end;
    end;
  end;

  WriteLn(Format('Step Row %d Col %d Phase %d Fit %d WrongCount %d',
   [State.CurrentRow, State.CurrentCol, Byte(State.CurrentPhase),
    State.BestFit, State.WrongCount]));

  Drawer.UpdateImage;
  OnUpdateUI(Self);
end;

function TTestLogic.GetNextHigherSize(var ASize: Word): Boolean;
begin


  Result := True;
end;

function TTestLogic.GetNextLowerSize(var ASize: Word): Boolean;
begin
  Result := True;
end;

procedure TTestLogic.HandleKeyPress(Sender: TObject; var Key: char);
begin
  WriteLn('TTestLogic.KeyPress Key=' + Key);
  case Key of
  '2': NextStep(dSouth);
  '4': NextStep(dWest);
  '5': NextStep(dNone);
  '6': NextStep(dEast);
  '8': NextStep(dNorth);
  else
    Exit;
  end;

  Drawer.UpdateImage;
end;

procedure TTestLogic.HandleKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  WriteLn('TTestLogic.KeyDown Key=' + DbgsVKCode(Key));
  case Key of
  VK_DOWN: NextStep(dSouth);
  VK_LEFT: NextStep(dWest);
  VK_RIGHT: NextStep(dEast);
  VK_UP: NextStep(dNorth);
  else
    Exit;
  end;

  Drawer.UpdateImage;
end;

procedure TTestLogic.HandleKeyUp(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  WriteLn('TTestLogic.KeyUp Key=' + DbgsVKCode(Key));
  case Key of
  VK_DOWN: NextStep(dSouth);
  VK_LEFT: NextStep(dWest);
  VK_RIGHT: NextStep(dEast);
  VK_UP: NextStep(dNorth);
  else
    Exit;
  end;

  Drawer.UpdateImage;
end;

initialization

  gTestLogic := TTestLogic.Create;

finalization

  gTestLogic.Free;

end.

